<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="initial-scale=1, maximum-scale=1,user-scalable=no"/>
<title>Geometry Engine Showcase</title>
<link rel="stylesheet" href="http://js.arcgis.com/3.15/esri/css/esri.css">
<style>
html, body, #map {
  height: 100%;
  width: 100%;
  margin: 0;
  padding: 0;
}
#results{
  bottom: 20px;
  left: 20px;
  width: 150px;
}
#title{
  top: 20px;
  width: 50%; 
  height: 50px;;
  padding-top: 5px;
  text-align: center;
  margin: 0 0 0 25%;
}
.mainStyle{
  position: absolute;
  z-index: 99;
  background-color: white;
  border-radius: 8px;
  padding: 15px;
  opacity: 0.8;
}
</style>

<script>
var dojoConfig = { 
  paths: {
    data: location.pathname.replace(/\/[^/]+$/, "") + "/data"
  }
};
</script>

<script src="http://js.arcgis.com/3.15/"></script>
<script>
var map, orchardLayer;
require(["esri/map",
         "esri/graphic",
         "esri/graphicsUtils",
         "esri/geometry/Extent",
         "esri/geometry/geometryEngine",
         "esri/geometry/Polyline",
         "esri/geometry/Point",
         "esri/SpatialReference",
         "esri/toolbars/draw",
         "esri/symbols/SimpleFillSymbol",
         "esri/symbols/SimpleLineSymbol",
         "esri/symbols/TextSymbol",
         "esri/symbols/Font",
         "esri/Color",
         "esri/renderers/SimpleRenderer",
         "esri/layers/GraphicsLayer",
         "data/orchards",  //Feature layer representing an apple orchard
         "dojo/on",
         "dojo/_base/array",
         "dojo/dom",
         "dojo/domReady!"
], function(Map, Graphic, graphicsUtils, Extent, geometryEngine, Line, Point, SpatialReference, Draw, SimpleFillSymbol, SimpleLineSymbol, TextSymbol, Font, Color, SimpleRenderer, GraphicsLayer, orchards, on, array, dom) {
    
  var initExtent = new Extent(-13056650, 6077558, -13055709, 6077938, new SpatialReference({wkid:3857}));
    
  //Instructions for using the app. Will update based on user behavior    
  var drawInstructions = "Click outside the selected feature to begin drawing cutting line. Double-click to finish line and make final cut.";
  var initInstructions = "Click to select a feature.";
  var mergeInstructions = "Select another polygon to merge into the selected feature.";
  var addInstructions = "Click to begin drawing a new feature. Polygons must have at least 3 vertices.";
  var deleteInstructions = "Click to delete a feature.";
  dom.byId("instructions").innerHTML = initInstructions;
  map = new Map("map", {
    basemap: "satellite",
    extent: initExtent
  });
    
  //Layer symbology    
  var orchardSym = new SimpleFillSymbol(SimpleFillSymbol.STYLE_SOLID, new SimpleLineSymbol(SimpleLineSymbol.STYLE_SOLID, new Color([175, 255, 155]), 2), new Color([175, 255, 155, 0.3]));
  var selectionSym = new SimpleFillSymbol(SimpleFillSymbol.STYLE_SOLID, new SimpleLineSymbol(SimpleLineSymbol.STYLE_NULL, new Color("black"), 3), new Color([255, 255, 255, 0.25]));
  var offsetSym = new SimpleFillSymbol(SimpleFillSymbol.STYLE_NULL, new SimpleLineSymbol(SimpleLineSymbol.STYLE_DASH, new Color("#960AC4"), 2), new Color([96, 252, 219, 0.5]));
  var cutterSym = new SimpleLineSymbol(SimpleLineSymbol.STYLE_DASH, new Color("white"), 2);
  var renderer = new SimpleRenderer(orchardSym);
    
  //Get FeatureLayer created from FeatureCollection stored on the client through data/orchards
  orchardLayer = orchards;  
  orchardLayer.setRenderer(renderer);
    
  //Create graphics layer for labeling
  var displayLabels = new GraphicsLayer();
  var labelFont = new Font(14, Font.STYLE_NORMAL, Font.VARIANT_NORMAL, Font.WEIGHT_BOLD, "Verdana");    
  var textSym = new TextSymbol("filler text", labelFont, new Color("white")).setHaloColor(new Color("#960AC4")).setHaloSize(2);
    
  //Create selection layer to display selection symbol
  var selectionLayer = new GraphicsLayer();
  var originalGraphicSelection, selectedGraphic;

  //Create GraphicsLayer for cutting line   
  var cutGraphic1, cutGraphic2;

  map.addLayers([orchardLayer, selectionLayer, displayLabels]);
    
  //Initialize Draw polygon tool
  var drawPolygon = new Draw(map, { showTooltips: true });
  drawPolygon.setFillSymbol(new SimpleFillSymbol(SimpleFillSymbol.STYLE_SOLID, cutterSym, new Color([255, 255, 255, 0.25])));

  //Calculates the area of the input geometry using GeometryEngine.geodesicArea() and converts it to a string
  function calcArea(geom){
    return (Math.round(geometryEngine.geodesicArea(geom, 109402)*100) / 100) + " acres"; //area in acres
  }
    
  //Define cutting line object
  var cutter = null;
    
  //Function for creating drawing line
  function drawLine(pt1, pt2){
    var pt1Cor = [pt1.x, pt1.y];
    var pt2Cor = [pt2.x, pt2.y];
    var lineJSON = {
      paths: [[pt1Cor, pt2Cor]],
      spatialReference: pt1.spatialReference
    };
    var polyLine = new Line(lineJSON);
    return polyLine;
  }
  
  //Label areas based on input POLYGON geometries
  function labelAreas(geoms){
     displayLabels.clear();
     array.forEach(geoms, function(item, i){
        var center = item.getCentroid();
        var area = calcArea(item);
        textSym.setText(area);
        displayLabels.add(new Graphic(center, textSym));
     });
  }
    
  var isSelected = false;
  
  //Select features on click event    
  function selectFeature(evt){
    if(cutOpt.checked){
      disableMerge();
      disableAdd();
      disableDelete();
      dom.byId("instructions").innerHTML = drawInstructions;    
      if(!disableDraw){
          return;
      }
      selectionLayer.clear();    
      originalGraphicSelection = evt.graphic;
      selectedGraphic = new Graphic(evt.graphic.geometry, selectionSym);
      selectionLayer.add(selectedGraphic);
      isSelected = true;   
    }
    if(mergeOpt.checked){
      disableCut();
      disableAdd();
      disableDelete();
      selectedGraphic = new Graphic(evt.graphic.geometry, selectionSym);
      selectionLayer.add(selectedGraphic);
      dom.byId("instructions").innerHTML = mergeInstructions;
    }  
    return selectedGraphic;
  }
  
  //Manage map clicking for line drawing purposes    
  var initClick;
  var clicks = 0;
    
  //cut and merge options    
  var cutOpt = dom.byId("cutOpt");
  var mergeOpt = dom.byId("mergeOpt");
  var addOpt = dom.byId("addOpt");   
  var deleteOpt = dom.byId("deleteOpt");    
   
  //click handler    
  on(map, "click", mapClick);

  on(map, "layers-add-result", function(evt){
    var geoms = graphicsUtils.getGeometries(orchardLayer.graphics);
    labelAreas(geoms);
  });
    
  function initCut(){
    cutter = null;  //reset cutter to null each time cut is intialized
    disableDraw = false;  //enable drawing
    map.disableMapNavigation();  //disable map navigation while drawing  
  }
  
  //Starts drawing cutting line    
  function startCut(evt){
    if(clicks === 0){
      initCut();  
    }

    clicks++;  //increment map clicking var for line construction purposes  
    if(clicks === 1){
        initClick = evt.mapPoint;  //set initial click to global var for testing purposes
        if(selectedGraphic){
           var initClickOutsideSelection = geometryEngine.disjoint(selectedGraphic.geometry, initClick);
        }
        if(!initClickOutsideSelection){
            //Don't allow for cutting to being inside of selected feature
            alert("Start cutting line outside of selected feature.");
        }
        else{
            //Create line and add to map
            var lineGeom = drawLine(evt.mapPoint, evt.mapPoint);
            cutter = new Graphic(lineGeom, cutterSym);
            map.graphics.add(cutter);
        }
    }
    if(clicks > 1){
      //Add additional line paths for each click (can be "buggy" at times) don't click too fast
      cutter.geometry.addPath([cutter.geometry.paths[cutter.geometry.paths.length-1][1], evt.mapPoint]);
      map.graphics.redraw();
    } 
    return;   
  }
    
  //Click handler    
  function mapClick(evt){
    if(cutOpt.checked){
       activateCut(evt); 
    }
    if(mergeOpt.checked){
       activateMerge(evt);
    }
    if(addOpt.checked){
       disableCut();
       disableMerge();
       disableDelete();
    }
    if(deleteOpt.checked){
       activateDelete(evt);
    }
  }
    
  //Delete features when clicked    
  function activateDelete(evt){
    var feature = evt.graphic.geometry;
    if(feature){
      array.forEach(orchardLayer.graphics, function(item, i){
        if(item && geometryEngine.equals(item.geometry, feature)){
          orchardLayer.remove(item);
          displayLabels.remove(displayLabels.graphics[i]);
        }    
      });
    } else{
      alert("Select a feature to delete.");
    }
  }
   
  //Activate cutting tool    
  function activateCut(evt){
    if(evt.graphic && !isSelected){
      selectFeature(evt);
    } else if (!evt.graphic && !isSelected){
      alert("Please select a feature to cut first.");
      return;
    } 
    else if (evt.graphic && isSelected && clicks < 1){
      var sameGraphic = geometryEngine.equals(evt.graphic.geometry, selectedGraphic.geometry); 
      if(!sameGraphic){     
        startCut(evt); 
      } else{
          alert("You must start drawing outside of the feature to cut it.");
      }
    }
    else{
      startCut(evt);
    } 
  }    
    
 //For managing cleared selections   
 function clearSelectedFeatures(){
      selectionLayer.clear();
      isSelected = false;
      clicks = 0;
      cutter = null;
      selectedGraphic = null;
 }
    
  //After starting to draw a line, continue drawing with cursor movement    
  on(map, "mouse-move", function(evt){
    if(cutter && !disableDraw){
        cutter.geometry.setPoint(cutter.geometry.paths.length-1, 1, evt.mapPoint);
        map.graphics.redraw();
        cutGeoms(selectedGraphic.geometry, cutter.geometry, evt.mapPoint, selectionLayer);
    }
    else{
        return;
    }
  });
    
 var disableDraw = true;
    
 //Finish drawing line on double-click event 
 on(map, "dbl-click", function(evt){
   if(cutOpt.checked){
     map.enableMapNavigation();
     disableDraw = true;
     map.graphics.clear();
     var goodCut = cutGeoms(selectedGraphic.geometry, cutter.geometry, evt.mapPoint, orchardLayer);
     if(goodCut){
        //Add cut graphics to map and remove original selected graphic
        orchardLayer.add(cutGraphic1);
        orchardLayer.add(cutGraphic2);
        orchardLayer.remove(originalGraphicSelection);
      }
      else{
        disableDraw = true;
      }
      clearSelectedFeatures();
      dom.byId("instructions").innerHTML = initInstructions;
      enableMerge();
      enableAdd();
      enableDelete();
   }
   if(addOpt.checked){
     drawPolygon.finishDrawing();
   }
   var orchardGeoms = graphicsUtils.getGeometries(orchardLayer.graphics);
   labelAreas(orchardGeoms);     
  });
    
  //cut the polygon with the line drawn on map
  function cutGeoms(poly, cutLine, endPoint, lyr){
      var crossTest = geometryEngine.crosses(cutLine, poly);
      var disjointTest = geometryEngine.disjoint(poly, endPoint);
      //Don't attempt cut if line doesn't properly cross selected geometry
      if(crossTest && disjointTest){
        var cutPolys = geometryEngine.cut(poly, cutLine);
        if(lyr === selectionLayer){
            lyr.clear();
            //Define final cut graphics and add temporary ones to map as mouse moves
            cutGraphic1 = new Graphic(cutPolys[0], orchardSym);
            cutGraphic2 = new Graphic(cutPolys[1], orchardSym);
            lyr.add(new Graphic(cutPolys[0], selectionSym));
            lyr.add(new Graphic(cutPolys[1], selectionSym));

            //Create dashed offset polygon symbol and add to map for visualization purposes
            var offsetPolys = offsetGeoms(cutPolys);
            lyr.add(new Graphic(offsetPolys[0], offsetSym));
            lyr.add(new Graphic(offsetPolys[1], offsetSym));
            labelAreas(cutPolys);
        }
        if(lyr === orchardLayer){
            //For finishing the line
            selectionLayer.clear();
        }
        return true;
      }
      else{
          return false;
      }
  }
    
  var selections = [];    
  var selection1, selection2;
    
  function activateMerge(evt){
    if(orchardLayer.graphics.length < 2){
      alert("There must be more than one graphic available to perform a merge operation.");
    } else if(!evt.graphic){
      alert("You must select a feature to merge");
    } else { 
      selections.push(selectFeature(evt));
      if(selections.length === 1){
        selection1 = evt.graphic;
      }
      if (selections.length === 2){
        selection2 = evt.graphic;
        mergeGeoms(selections);
        selections = [];
      }
    }
  }    
  
  //Merge polygon geometries    
  function mergeGeoms(graphs){
    geom1 = graphs[0].geometry;
    geom2 = graphs[1].geometry;
    var mergedGeometry = geometryEngine.union([geom1, geom2]);
    selectionLayer.clear();
    orchardLayer.remove(selection1);
    orchardLayer.remove(selection2);
    orchardLayer.add(new Graphic(mergedGeometry, orchardSym));
    labelAreas(graphicsUtils.getGeometries(orchardLayer.graphics));
    dom.byId("instructions").innerHTML = initInstructions;
    enableCut();
    enableAdd();
    enableDelete();
  }
    
  on(addOpt, "click", function(evt){
    if(addOpt.checked){
      activateAdd();
    }
    dom.byId("instructions").innerHTML = addInstructions;
  });
    
  on(cutOpt, "click", function(evt){
    deactivateAdd();
    dom.byId("instructions").innerHTML = initInstructions;  
  });
    
  on(mergeOpt, "click", function(evt){
    deactivateAdd();
    dom.byId("instructions").innerHTML = initInstructions;
  });
    
  on(deleteOpt, "click", function(evt){
    deactivateAdd();
    dom.byId("instructions").innerHTML = deleteInstructions;
  });    
    
  on(drawPolygon, "draw-end", function(evt){
    enableCut();
    enableMerge();
    enableDelete();
    deactivateAdd();
    activateAdd();
    var geom = evt.geometry;
    if(geom.rings[0].length <= 3){
      alert("Polygon must have at least three vertices.");
      return;
    }  
    
    var topoTest = testTopology(geom);
    if(topoTest === "within"){
      alert("New feature must not be completely within existing features.");
    }
    if(typeof topoTest === "object"){
      orchardLayer.add(new Graphic(topoTest, orchardSym));
    }
  });
    
  function testTopology(feature){
      var newfeature, combined;
      var allGeoms = graphicsUtils.getGeometries(orchardLayer.graphics);
    if(allGeoms.length > 0){
      combined = geometryEngine.union(allGeoms);
      if(geometryEngine.within(feature, combined)){
        return "within";
      }
      if(geometryEngine.intersects(combined, feature)){
        newfeature = geometryEngine.difference(feature, combined);
      }
      else{
        newfeature = feature;
      }
    }
    else{
      newfeature = feature;
    }
    return newfeature;
  }
    
  function activateAdd(){
    drawPolygon.activate(Draw.POLYGON);
  }
    
  function deactivateAdd(){
    drawPolygon.deactivate();
  }    

  //Function for offseting geometries by 10 feet    
  function offsetGeoms(polys){
    return geometryEngine.offset(polys, 10, 9002, 0);
  }
  
  //Prevent user from changing tools mid-selection    
  function disableMerge(){
    mergeOpt.disabled = true;
    dom.byId("mergeLabel").style.color = "gray";
  }
  function enableMerge(){
    mergeOpt.disabled = false;
    dom.byId("mergeLabel").style.color = "black";  
  } 
  function disableCut(){
    cutOpt.disabled = true;
    dom.byId("cutLabel").style.color = "gray";
  }
  function enableCut(){
    cutOpt.disabled = false;
    dom.byId("cutLabel").style.color = "black";  
  }
  function disableAdd(){
    addOpt.disabled = true;
    dom.byId("addLabel").style.color = "gray";
  }
  function enableAdd(){
    addOpt.disabled = false;
    dom.byId("addLabel").style.color = "black";  
  } 
  function disableDelete(){
    deleteOpt.disabled = true;
    dom.byId("deleteLabel").style.color = "gray";
  }
  function enableDelete(){
    deleteOpt.disabled = false;
    dom.byId("deleteLabel").style.color = "black";  
  }    
});
</script>
</head>

<body>
  <div id="map">
    <div class="mainStyle" id="title"><h2>Apple Orchards</h2></div>
    <div class="mainStyle" id="results"><span id="instructions"></span><br><br>
    <input type="radio" name="operation" id="cutOpt" checked><span id="cutLabel">Cut Features</span><br><br>
    <input type="radio" name="operation" id="mergeOpt"><span id="mergeLabel">Merge Features</span><br><br>
    <input type="radio" name="operation" id="addOpt"><span id="addLabel">Add Features</span><br><br>
    <input type="radio" name="operation" id="deleteOpt"><span id="deleteLabel">Delete Features</span><br><br>     
  </div>
</div>
</body>
</html>